/* $Id$ */
/*===========================================================================
   This is tlog.m, derived from slog.m by Scott Hess.  His original 
   header is included below.  The parent process uses the DO system to
   talk to the TLogger object, rather than the Speaker/Listener mechanism.

   The program requires to be installed as suid to root.tty to run
   correctly.

   Pedja <pedja@TipTop.com>
===========================================================================*/

/* Replacement for StuartLog in Stuart2.4.
 *
 * This program is in the public domain.  Use and abuse it as you see fit.
 *
 * This program requires setuid-root to run correctly.  There's
 * now a Makefile that sets all of that up - so use make to compile
 * this.  There will be warnings under NeXSTEP3.0.  Of course,
 * consult Stuart's online documentation under Installation/slog
 * for more information.
 *
 *
 * The StuartLog tool in Stuart2.3 was a good idea, but it didn't
 * quite work.  I found that it would often simply hang during the
 * login process, thus either hanging Stuart or not getting the
 * logging functions done.  slog takes a new approach.  It runs
 * continuously from the time Stuart is launched, and the same process
 * handles all logging functions.  Stuart communicates with slog
 * via a private Speaker/Listener pair.  slog also monitors the
 * parent Stuart process and exits on abnormal termination.
 *
 * Admire the code to connect to the parent process.  I think it
 * cost me a kidney.
 *
 * scott hess
 * shess@ssesco.com
 */
#import <objc/HashTable.h>
#import <libc.h>
#import <grp.h>
#import <lastlog.h>
#import <utmp.h>
#import <ttyent.h>
#import <pwd.h>
#import <mach/mach.h>
#import <mach/mach_error.h>
#import <dpsclient/dpsclient.h>
#import <mach/notify.h>

#import <remote/NXConnection.h>
#import <machkit/NXPort.h>

#import "tlog.h"

@interface TLogger : Object <TLoggerProtocol>
{
    HashTable *slots;
    int uid;
    const char *name;
    port_t parentNotify;
    NXConnection *connection;
}
- run;
@end

/* Locking open and close.  Though flock() is not a good general-purpose
 * file locker due to NFS limitations.  It works well for this case
 * since only the local machine can access the devices.
 */
int lopen( const char *filename, int openFlags)
{
    int fd=open( filename, openFlags);
    flock( fd, LOCK_EX);
    return fd;
}
int lclose( int fd)
{
    flock( fd, LOCK_UN);
    return close( fd);
}
/* Fix ownerships and permissions on the named pty line. */
void fixOwnership( const char *pty, int uid, int gid, int mod)
{
    char dev[ 64];
    sprintf( dev, "/dev/%s", pty);
    chown( dev, uid, gid);
    chmod( dev, mod);
}
/* Write an entry to wtmp. */
void writeWtmp( struct utmp *ut)
{
    int f=lopen( "/usr/adm/wtmp", O_WRONLY | O_APPEND);
    if( f>=0) {
	write( f, ut, sizeof( struct utmp));
	lclose( f);
    } else {
	perror( "opening /usr/adm/wtmp");
    }
}
/* Write an entry to utmp. */
void writeUtmp( struct utmp *ut, int slot)
{
    if( slot>-1) {
	int f=lopen( "/etc/utmp", O_WRONLY);
	if( f>=0) {
	    lseek( f, slot*sizeof( struct utmp), L_SET);
	    write( f, ut, sizeof( struct utmp));
	    lclose( f);
	} else {
	    perror( "opening /etc/utmp");
	}
    }
}

@implementation TLogger
/* Initialize uid to an invalid user id (0 is valid). */
- init
{
    self=[super init];
    if( self) {
	uid=-1;
    }
    return self;
}
/* Find the slot in the /etc/ttys file for the given device.  Cache
 * a mapping from the device name to the slot number for future use.
 */
-(int)getSlot:(const char *)device
{
    if( ![slots isKey:device]) {
	struct ttyent *t;
	int slot;
	
	setttyent();
	for( slot=1; t=getttyent(); slot++) {
	    if( !strcmp( device, t->ty_name)) {
		break;
	    }
	}
	endttyent();
	if( !t) {
	    slot=-1;
	}
	if( !slots) {
	    slots=[HashTable allocFromZone:[self zone]];
	    slots=[slots initKeyDesc:"*" valueDesc:"i" capacity:0];
	}
	device=NXUniqueString( device);
	[slots insertKey:device value:(void *)slot];
	return slot;
    } else {
	return (int)[slots valueForKey:device];
    }
}
/* Login a use on the given pty. */
-(int)login:(const char *)pty ownerships:(int)ownership
       utmp:(int)utmp wtmp:(int)wtmp lastlog:(int)lastlog
{
#ifdef DEBUG
  printf("tlog: login:%s ownerships:%d utmp:%d wtmp:%d lastlog:%d\n",
	 pty,ownership,utmp,wtmp,lastlog);
#endif
    /* Cache a passwd entry for the user if needed. */
    if( uid==-1) {
	/* Grab a passwd entry, set up uid.  This code was suggested
	 * by der Mouse <mouse@larry.mcrcim.mcgill.edu>
	 */
	char *user=getenv( "USER");
	struct passwd *pw=NULL;
	uid=getuid();
	if( user) {
	    pw=getpwnam( user);
	}
	if( !pw || (uid && (uid!=pw->pw_uid))) {
	    pw=getpwuid( uid);
	}
	if( pw) {
	    uid=pw->pw_uid;
	}
	if( pw) {
	    name=NXUniqueString( pw->pw_name);
	} else {
	    name="Unknown";
	}
    }
    if( utmp || wtmp || lastlog) {
	struct utmp ut;

	/* Clean up the utmp entry. */
	bzero( &ut, sizeof( ut));

	/* Set up the ut_name field if necessary. */
	if( wtmp || utmp) {
	    strncpy( ut.ut_name, name, sizeof( ut.ut_name));
	}

	/* Setup the line and time. */
	strncpy( ut.ut_line, pty, sizeof( ut.ut_line));
	time( &( ut.ut_time));

	/* Log to lastlog as needed. */
	if( lastlog) {
	    int f=lopen( "/usr/adm/lastlog", O_WRONLY);
	    if( f>=0) {
		struct lastlog llog;
		bzero( &llog, sizeof( llog));
		llog.ll_time=ut.ut_time;
		strncpy( llog.ll_line, ut.ut_line, sizeof( llog.ll_line));
		lseek( f, uid*sizeof( llog), L_SET);
		write( f, &llog, sizeof( llog));
		lclose( f);
	    } else {
	        perror( "opening /usr/adm/lastlog");
	    }
	}
	
	/* Log to utmp and wtmp as needed. */
	if( utmp) {
	    writeUtmp( &ut, [self getSlot:pty]);
	}
	if( wtmp) {
	    writeWtmp( &ut);
	}
    }
    
    /* If needed, set pty ownership to the new user, with permissions
     * set for owner read/write, group write.  Group ownership set
     * to the tty group, if available.
     */
    if( ownership) {
	struct group *gr=getgrnam( "tty");
	fixOwnership( pty, uid, gr ? gr->gr_gid : -1, 0620);
    }
#ifndef DO_FIXED
    if(pty) free((void*)pty);
#endif
    return 0;
}
-(int)login:(const char *)pty ownerships:(int)ownership utmp:(int)utmp
{
#if 0
    return [self login:pty ownerships:ownership utmp:utmp wtmp:utmp lastlog:utmp];
#else			/* This may make more sense. */
    return [self login:pty ownerships:ownership utmp:utmp wtmp:YES lastlog:YES];
#endif
}
-(int)logout:(const char *)pty ownerships:(int)ownership
	utmp:(int)utmp wtmp:(int)wtmp lastlog:(int)lastlog
{
#ifdef DEBUG
  printf("tlog: logout:%s ownerships:%d utmp:%d wtmp:%d lastlog:%d\n",
	 pty,ownership,utmp,wtmp,lastlog);
#endif
    if( utmp || wtmp) {
	struct utmp ut;
	
	/* Clean up the utmp entry. */
	bzero( &ut, sizeof( ut));
	
	strncpy( ut.ut_line, pty, sizeof( ut.ut_line));
	time( &( ut.ut_time));
	if( utmp) {
	    writeUtmp( &ut, [self getSlot:pty]);
	}
	if( wtmp) {
	    writeWtmp( &ut);
	}
    }

    /* If needed, set pty ownership back to root user, with permissions
     * set for all read/write.  Group ownership reset to the tty
     * group, if available.
     */
    if( ownership) {
	struct group *gr=getgrnam( "tty");
	fixOwnership( pty, 0, gr ? gr->gr_gid : -1, 0666);
    }
#ifndef DO_FIXED
    if(pty) free((void*)pty);
#endif
    return 0;
}
-(int)logout:(const char *)pty ownerships:(int)ownership utmp:(int)utmp
{
#if 0
    return [self logout:pty ownerships:ownership utmp:utmp wtmp:utmp lastlog:utmp];
#else			/* This may make more sense. */
    return [self logout:pty ownerships:ownership utmp:utmp wtmp:YES lastlog:YES];
#endif
}
/* I use this routine to let Stuart _kindly_ ask slog to exit.  I
 * don't want Stuart doing a kill() on slog while slog's in the middle
 * of something ...
 */
-(oneway void)exit
{
#ifdef DEBUG
  printf("exit...\n");
#endif
    exit( 0);
}
/* Disconnect our controlling tty and connect to the console device. */
- ttyDisconnect
{
    int tty;

    tty=open( "/dev/tty", O_RDWR);
    if( tty>-1) {
	ioctl( tty, TIOCNOTTY, 0);
	close( tty);
    }
    tty=open( "/dev/console", O_WRONLY);
    setpgrp( 0, getpid());
    dup2( tty, 1);
    dup2( tty, 2);
    if( tty!=1 && tty!=2) {
	close( tty);
    }
    return self;
}

// FIXME: This is never invoked, since [NXConnection run] does not dispatch
// notify events...
/* Catch inadvertant parent process death. */
void notifyPortHandler( notification_t *msg, TLogger *self)
{
#ifdef DEBUG
  printf("Parent died!\n");
#endif
    if( msg->notify_header.msg_id==NOTIFY_PORT_DELETED) {
	if( msg->notify_port==self->parentNotify) {
	    [self exit];
	}
    }
}
- run
{
    kern_return_t ret;
    msg_header_t initMsg;
    extern int getppid( void);
    task_t parentTask;
    port_t notify;

    [self ttyDisconnect];

    connection=[NXConnection registerRoot:self];
    /* Give us plenty of leeway for when people logout.
     * Actually, even this isn't really that great,
     * but what can you do?
     */
    port_set_backlog(task_self(),[[connection inPort] machPort],
		     PORT_BACKLOG_MAX);

    /* Find our parent's notify port. */
    ret=task_by_unix_pid( task_self(), getppid(), &parentTask);
    if( ret!=KERN_SUCCESS) {
	printf( "tlog: Unable to get parent's task_t.\n");
	exit( 1);
    }
    ret=task_get_notify_port( parentTask, &parentNotify);
    initMsg.msg_remote_port=parentNotify;
    if( ret!=KERN_SUCCESS) {
	printf( "tlog: Unable to get parent's notify port.\n");
	exit( 1);
    }
    port_allocate( task_self(), &notify);
    task_set_notify_port( task_self(), notify);
    DPSAddNotifyPortProc((DPSPortProc)notifyPortHandler,self);

    /* Set up the rest of the header. */
    initMsg.msg_simple=TRUE;
    initMsg.msg_size=sizeof( initMsg);
    initMsg.msg_type=MSG_TYPE_NORMAL;
    initMsg.msg_id=0;
    
    /* Including the port which our Listener listens on. */
    initMsg.msg_local_port=[[connection inPort] machPort];

    /* Send it, and if successful, enter the event loop. */
    ret=msg_send( &initMsg, SEND_TIMEOUT, 30000);

    if(ret==KERN_SUCCESS) [connection run];
    else {
      printf( "tlog: Unable to send Listener port to parent.\n");
      exit(1);
    }
    return self;
}
@end

void main(void)
{
#ifdef DEBUG
  printf("tlog:run\n");
#endif
  [[[TLogger alloc] init] run];
}
